package bancosys.tec.services.implementor.descriptor;

import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;

import jmine.tec.cache.ExpiringReference;
import jmine.tec.di.resolver.ObjectResolver;
import jmine.tec.di.scope.TypedScope;
import jmine.tec.di.type.TypeConvertionException;
import jmine.tec.di.wrapper.Wrapper;
import jmine.tec.di.wrapper.Wrappers;

import org.hibernate.HibernateException;
import org.hibernate.LockMode;
import org.hibernate.SessionFactory;

import bancosys.tec.persist.Persistable;

/**
 * {@link ObjectResolver} que guarda objetos resolvidos
 * 
 * @author takeshi
 */
public class CachedObjectResolver implements ObjectResolver {

    private final ObjectResolver delegate;

    private static final ThreadLocal<Map<CacheKey, ExpiringReference<CacheEntry>>> CACHE_MAP =
            new ThreadLocal<Map<CacheKey, ExpiringReference<CacheEntry>>>();

    private ObjectResolver sessionFactoryResolver;

    private final long timeout;

    private final String name;

    public static final AtomicLong CACHE_HIT_COUNT = new AtomicLong();

    public static final AtomicLong CACHE_MISS_COUNT = new AtomicLong();

    public static final AtomicLong STALE_ENTRIES_REMOVED_COUNT = new AtomicLong();

    private static final boolean LOG_ENABLED = Boolean.valueOf(System.getProperty("LOG_ENABLED", "false"));

    /**
     * The cachekey
     * 
     * @author takeshi
     */
    private static class CacheKey {
        private final String value;

        private CachedObjectResolver resolver;

        /**
         * @param value o nome do valor
         * @param resolver o resolver
         */
        public CacheKey(String value, CachedObjectResolver resolver) {
            super();
            this.value = value;
            this.resolver = resolver;
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public int hashCode() {
            final int prime = 31;
            int result = 1;
            result = prime * result + ((this.resolver == null) ? 0 : this.resolver.hashCode());
            result = prime * result + ((this.value == null) ? 0 : this.value.hashCode());
            return result;
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public boolean equals(Object obj) {
            if (this == obj) {
                return true;
            }
            if (obj == null) {
                return false;
            }
            if (this.getClass() != obj.getClass()) {
                return false;
            }
            CacheKey other = (CacheKey) obj;
            if (this.resolver == null) {
                if (other.resolver != null) {
                    return false;
                }
            } else if (!this.resolver.equals(other.resolver)) {
                return false;
            }
            if (this.value == null) {
                if (other.value != null) {
                    return false;
                }
            } else if (!this.value.equals(other.value)) {
                return false;
            }
            return true;
        }

    }

    /**
     * @param delegate the {@link ObjectResolver}
     * @param sessionFactoryResolver SessionFactoryResolver
     * @param timeout the long
     * @param name {@link String}
     */
    public CachedObjectResolver(ObjectResolver delegate, ObjectResolver sessionFactoryResolver, long timeout, String name) {
        super();
        this.name = name;
        this.timeout = timeout;
        this.sessionFactoryResolver = sessionFactoryResolver;
        this.delegate = delegate;
    }

    /**
     * The cache entry
     */
    private static class CacheEntry {
        private Class<?> requestedType;

        private Object value;

        private Object staleCheck;

        /**
         * {@inheritDoc}
         */
        @Override
        public String toString() {
            return super.toString();
        }

    }

    /**
     * {@inheritDoc}
     */
    public boolean isResolvable(TypedScope properties) {
        return this.delegate.isResolvable(properties);
    }

    /**
     * {@inheritDoc}
     */
    public boolean isStatic() {
        return this.delegate.isStatic();
    }

    /**
     * {@inheritDoc}
     */
    public Object resolveObject(TypedScope properties, Wrapper wrapper, Class<?> requiredComponentClass) throws TypeConvertionException {
        Object val = properties.getProperties().get(this.name);
        if (val instanceof String && !(String.valueOf(val)).startsWith("$")) {
            final String rawValue = String.valueOf(val);
            CacheEntry entry = this.getCacheEntry(rawValue);
            if (!this.isStale(entry) && entry.requestedType == requiredComponentClass) {
                if (LOG_ENABLED) {
                    CACHE_HIT_COUNT.getAndIncrement();
                }
                this.lockIfNecesary(entry);
                return entry.value;
            } else {
                if (LOG_ENABLED) {
                    if (entry != null) {
                        CACHE_MISS_COUNT.getAndIncrement();
                    }
                }
            }
            Object value = this.delegate.resolveObject(properties, wrapper, requiredComponentClass);
            this.setCache(requiredComponentClass, value, rawValue);
            return value;
        } else {
            return this.delegate.resolveObject(properties, wrapper, requiredComponentClass);
        }

    }

    /**
     * {@inheritDoc}
     */
    public boolean isTypeCompatible(Class<?> declaredRawType) {
        return this.delegate.isTypeCompatible(declaredRawType);
    }

    /**
     * @param v the value
     * @return {@link CacheEntry}
     */
    private CacheEntry getCacheEntry(String v) {
        Map<CacheKey, ExpiringReference<CacheEntry>> map = this.getOrCreateCacheMap();
        final CacheKey cacheKey = this.cacheKeyFor(v);
        final ExpiringReference<CacheEntry> entry = map.get(cacheKey);
        if (entry == null) {
            return null;
        }
        final CacheEntry val = entry.getReferentRenewTimeout();
        if (entry.isExpired()) {
            map.remove(cacheKey);
            if (LOG_ENABLED) {
                STALE_ENTRIES_REMOVED_COUNT.getAndIncrement();
            }
        }
        return val;
    }

    /**
     * @param v the value
     * @return {@link CacheKey}
     */
    private CacheKey cacheKeyFor(String v) {
        return new CacheKey(v, this);
    }

    /**
     * @return {@link Map} de {@link CacheKey}
     */
    private Map<CacheKey, ExpiringReference<CacheEntry>> getOrCreateCacheMap() {
        Map<CacheKey, ExpiringReference<CacheEntry>> map = CACHE_MAP.get();
        if (map == null) {
            map = new ConcurrentHashMap<CacheKey, ExpiringReference<CacheEntry>>();
            StaleEntriesRemover.scheduleRemoval(map);
            CACHE_MAP.set(map);
        }
        return map;
    }

    /**
     * @param type the {@link Class}
     * @param value the {@link Object}
     * @param rawValue {@link String}
     */
    private void setCache(Class<?> type, Object value, String rawValue) {
        CacheEntry entry = new CacheEntry();
        entry.requestedType = type;
        entry.value = value;
        entry.staleCheck = this.getStaleObjectChecker();
        final ExpiringReference<CacheEntry> ref = new ExpiringReference<CacheEntry>(entry, this.timeout);
        this.getOrCreateCacheMap().put(this.cacheKeyFor(rawValue), ref);
    }

    /**
     * Associate's the object with the current session if it's not already associated.
     * 
     * @param entry the cacheentry
     */
    private void lockIfNecesary(CacheEntry entry) {
        SessionFactory sf;
        try {
            sf = (SessionFactory) this.sessionFactoryResolver.resolveObject(null, Wrappers.NO_WRAPPER, SessionFactory.class);
            if (entry.value instanceof Persistable && !sf.getCurrentSession().contains(entry.value)) {
                sf.getCurrentSession().lock(entry.value, LockMode.NONE);
            }
        } catch (TypeConvertionException e) {
            throw new RuntimeException("Should never happen!", e);
        }
    }

    /**
     * check for staleness
     * 
     * @param entry the cacheentry
     * @return boolean
     */
    private boolean isStale(CacheEntry entry) {
        return entry == null || entry.staleCheck == null || entry.staleCheck != this.getStaleObjectChecker();
    }

    /**
     * @return {@link Object}
     */
    protected Object getStaleObjectChecker() {
        try {
            SessionFactory sf = (SessionFactory) this.sessionFactoryResolver.resolveObject(null, Wrappers.NO_WRAPPER, SessionFactory.class);
            return sf.getCurrentSession();
        } catch (HibernateException e) {
            // no hibernate session
            return null;
        } catch (TypeConvertionException e) {
            // cannot find session factory
            return null;
        }
    }

    /**
     * Guarda o {@link ScheduledExecutorService} usado para fazer a limpeza dos itens expirados
     * 
     * @author MarceloT
     */
    private static final class ExecutorHolder {

        /**
         */
        private ExecutorHolder() {
            super();
        }

        private static final ScheduledExecutorService EXECUTOR = Executors.newSingleThreadScheduledExecutor(new ThreadFactory() {
            public Thread newThread(Runnable r) {
                Thread t = new Thread(r);
                t.setDaemon(true);
                t.setPriority(Thread.MIN_PRIORITY);
                return t;
            }
        });

    }

    /**
     * {@link Runnable} para remover as entradas 'stale' do cache
     * 
     * @author takeshi
     */
    private static final class StaleEntriesRemover implements Runnable {
        private final Map<CacheKey, ExpiringReference<CacheEntry>> map;

        /**
         * @param map the {@link Map}
         */
        public StaleEntriesRemover(Map<CacheKey, ExpiringReference<CacheEntry>> map) {
            super();
            this.map = map;
        }

        /**
         * {@inheritDoc}
         */
        public void run() {
            Iterator<Entry<CacheKey, ExpiringReference<CacheEntry>>> iterator = this.map.entrySet().iterator();
            while (iterator.hasNext()) {
                if (iterator.next().getValue().isExpired()) {
                    iterator.remove();
                    if (LOG_ENABLED) {
                        STALE_ENTRIES_REMOVED_COUNT.getAndIncrement();
                    }
                }
            }
        }

        /**
         * Agenda uma remocao de entradas no mapa
         * 
         * @param map2 o mapa
         */
        private static void scheduleRemoval(Map<CacheKey, ExpiringReference<CacheEntry>> map2) {
            final int delay = 60;
            ExecutorHolder.EXECUTOR.scheduleWithFixedDelay(new StaleEntriesRemover(map2), delay, delay, TimeUnit.SECONDS);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String toString() {
        return "CachedObjectResolver[" + this.name + "]";
    }

}
